﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using IsmsExplorer.Model.Projects;
using IsmsExplorer.Model.AssetRegister;
using System.Diagnostics;
using IsmsExplorer.Model.RiskRegister;
using IsmsExplorer.Model.Iso27001;
using IsmsExplorer.Infrastructure.Helper;
using IsmsExplorer.Application.AssetRegister;
using IsmsExplorer.Application.Iso27001;
using IsmsExplorer.Application.RiskRegister;
using IsmsExplorer.Application.Projects;

namespace IsmsExplorer.IsmsDataImport.Parser {
    /// <summary>
    /// Parses asset tab-delimited text file and creates the ISMS information.
    /// The file is expected to contain full ASSET information on each strLine.
    /// 
    /// The fields (columns) are expected to be in the following order:
    /// 
    /// ASSET Category
    /// ASSET (name)
    /// OWNER
    /// LOCATION
    /// Value C-I-A (int)
    /// Threat Identification
    /// VULNERABILITY Description
    /// VULNERABILITY (int)
    /// LIKELIHOOD (int)
    /// IMPACT (int)
    /// Risk Exposure (ignored as it's calculated)
    /// Treatment Required (Y/N) (ignored as it's calculated)
    /// Treatment Selected (string, space delimited ISO 27001 control reference IDs of the form "A x.y.z" where x,y,z integers
    /// REFERENCE 
    /// V (int)
    /// L (int)
    /// I (int)
    /// Managed Risk Exposure (ignored as it's calculated)
    /// 
    /// </summary>
    internal class AssetRiskRegisterDataParser {
        // *************************************** Data ***************************************
        private string _fileName;
        private Project _project;
        private char _fieldSeparator = '\t';

        const string ASSET_CATEGORY = "AssetCategory";
        const string ASSET = "Asset";
        const string OWNER = "Owner";
        const string LOCATION = "Location";
        const string VALUE_CIA = "ValueCIA";
        const string THREAT_IDENTIFICATION = "ThreatIdentification";
        const string VULNERABILITY_DESCRIPTION = "VulnerabilityDescription";
        const string VULNERABILITY = "Vulnerability";
        const string LIKELIHOOD = "Likelihood";
        const string IMPACT = "Impact";
        const string RISK_EXPOSURE = "RiskExposure";
        const string TREATMENT_REQUIRED = "TreatmentRequired";
        const string TREATMENT_SELECTED = "TreatmentSelected";
        const string REFERENCE = "Reference";
        const string V = "V";
        const string L = "L";
        const string I = "I";
        const string MANAGED_RISK_EXPOSURE = "ManagedRiskExposure";

        const string NOT_APPLICABLE = "N/A";

        string[] _fields = new string[] { ASSET_CATEGORY, ASSET, OWNER, LOCATION, VALUE_CIA, THREAT_IDENTIFICATION, VULNERABILITY_DESCRIPTION, 
            VULNERABILITY, LIKELIHOOD, IMPACT, RISK_EXPOSURE, TREATMENT_REQUIRED, TREATMENT_SELECTED, REFERENCE, V, L, I, MANAGED_RISK_EXPOSURE };

        Dictionary<string, int> _fieldNameToOrder = new Dictionary<string, int>();
        Dictionary<string, Asset> _assetCache = new Dictionary<string, Asset>();

        // *************************************** Construction ***************************************
        public AssetRiskRegisterDataParser() {
            for (int i = 0; i < _fields.Length; i++) {
                _fieldNameToOrder.Add(_fields[i], i);
            }
        }

        // *************************************** Operations ***************************************

        public char FieldSeparator {
            get { return _fieldSeparator; }
            set { _fieldSeparator = value; }
        }

        public Project Project {
            get { return _project; }
            set { _project = value; }
        }

        public string FileName {
            get { return _fileName; }
            set { _fileName = value; }
        }

        public int GetNumberOfFields() {
            return _fields.Length;
        }

        public int GetFieldIndex(string fieldName) {
            int index = -1;
            _fieldNameToOrder.TryGetValue(fieldName, out index);
            return index;
        }

        private void Validate() {
            if (!File.Exists(FileName)) {
                throw new ArgumentException(string.Format("File {0} does not exist", FileName));
            }
            if (Project == null || Project.ID <= 0) {
                throw new ArgumentException("No Project has been specified");
            } else {
                if (ProjectService.GetProjectByID(this.Project) == null) {
                    throw new ArgumentException(string.Format("Project ID {0} does not exist in database", this.Project.ID));
                }
            }
        }

        public void Parse() {
            Validate();
            InitialiseHelperStructures();
            using (TextReader tr = new StreamReader(this.FileName)) {
                int lineCount = 0;
                try {
                    string strLine = null;
                    while ((strLine = tr.ReadLine()) != null) {
                        ++lineCount;
                        strLine = strLine.Trim();
                        // NOTE: we do not want to use the option to remove empty entries as they are occasional empty entries
                        //       e.g. the REFERENCE field and we validate against the number of expected fields.
                        string[] arrFields = strLine.Split(new char[] { this.FieldSeparator });
                        ValidateLine(lineCount, arrFields);
                        // Create the Asset (or load from cache if already in DB).
                        Asset asset = CreateAssetInformation(lineCount, arrFields);
                        // Create the Threat to the Asset.
                        Threat threat = CreateThreat(lineCount, arrFields, asset);
                        // Add to list of Threats for asset.
                        asset.AddThreat(threat);
                        // Get the Treatment for Threat.
                        Treatment treatment = CreateTreatment(lineCount, arrFields, threat);
                        threat.Treatment = treatment;
                        // Get all the controls applied via this Treatment.
                        CreateAppliedIsoControlsForTreatment(lineCount, arrFields, treatment);
                    }

                } catch (Exception ex) {
                    throw new Exception(PrintErrorAtLine(lineCount, ex.Message));
                }
            }
        }

        private void CreateAppliedIsoControlsForTreatment(int lineCount, string[] arrFields, Treatment treatment) {
            string strTreatmentSelected = arrFields[GetFieldIndex(TREATMENT_SELECTED)].Trim();
            // Expect either "N/A" or iso control reference IDs of the form "A x.y.z" separated by space.
            // We need to use space as separator so add a dot (.) after each A of each control so that space is only a delimiter
            // of reference IDs.
            // NOTE: in case the field contains only text, without any reference to ISO control, this is also ignored.
            if (strTreatmentSelected == NOT_APPLICABLE || Iso27001Helper.Matches(strTreatmentSelected).Count <= 0) {
                return;

            } else {
                strTreatmentSelected = strTreatmentSelected.Replace("A ", "A.");
                string[] refIDs = strTreatmentSelected.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries);
                if (refIDs.Length > 0) {
                    List<Iso27001Control> isoControls = new List<Iso27001Control>();
                    foreach (string s in refIDs) {
                        string refID = s.Trim();
                        if (!string.IsNullOrEmpty(refID)) {
                            if (!Iso27001Helper.Validate(refID)) {
                                throw new Exception(string.Format("Invalid ISO 27001 control reference ID found: '{0}'", refID));

                            } else {
                                Iso27001Control control = Iso27001ControlService.FindControlByReferenceID(refID);
                                if (control != null) {
                                    isoControls.Add(control);
                                } else {
                                    throw new Exception(string.Format("Cannot find ISO 27001 control for reference ID: '{0}'", refID));
                                }
                            }
                        }
                    }
                    // Associate the Iso controls found with the treatment.
                    RiskRegisterService.AssociateIsoControls(treatment, isoControls);

                } else {
                    throw new Exception("Expected to find Iso control reference IDs but none found");
                }
            }
        }

        private Treatment CreateTreatment(int lineCount, string[] arrFields, Threat threat) {
            string strReference = arrFields[GetFieldIndex(REFERENCE)].Trim();
            string strV = arrFields[GetFieldIndex(V)].Trim();
            string strL = arrFields[GetFieldIndex(L)].Trim();
            string strI = arrFields[GetFieldIndex(I)].Trim();
            int v = Convert.ToInt32(strV);
            int l = Convert.ToInt32(strL);
            int i = Convert.ToInt32(strI);
            Treatment t = new Treatment() {
                I = i,
                L = l,
                ProjectID = this.Project.ID,
                Reference = strReference,
                Threat = threat,
                V = v
            };
            RiskRegisterService.SaveTreatment(t);

            Debug.Assert(t.ID > 0);
            return t;
        }

        private Threat CreateThreat(int lineCount, string[] arrFields, Asset asset) {
            string strThreatIdentification = arrFields[GetFieldIndex(THREAT_IDENTIFICATION)].Trim();
            string strVulnerabilityDescription = arrFields[GetFieldIndex(VULNERABILITY_DESCRIPTION)].Trim();
            string strVulnerability = arrFields[GetFieldIndex(VULNERABILITY)].Trim();
            string strLikelihood = arrFields[GetFieldIndex(LIKELIHOOD)].Trim();
            string strImpact = arrFields[GetFieldIndex(IMPACT)].Trim();
            int vulnerability = Convert.ToInt32(strVulnerability);
            int likelihood = Convert.ToInt32(strLikelihood);
            int impact = Convert.ToInt32(strImpact);
            Threat threat = new Threat() {
                Asset = asset,
                Impact = impact,
                Likelihood = likelihood,
                ProjectID = this.Project.ID,
                ThreatDescription = strVulnerabilityDescription,
                ThreatIdentification = strThreatIdentification,
                Vulnerability = vulnerability
            };
            RiskRegisterService.SaveThreat(threat);

            Debug.Assert(threat.ID > 0);
            return threat;
        }

        private Asset CreateAssetInformation(int lineCount, string[] arrFields) {
            string strAssetCategory = arrFields[GetFieldIndex(ASSET_CATEGORY)].Trim();
            string strAssetName = arrFields[GetFieldIndex(ASSET)].Trim();
            string strAssetOwner = arrFields[GetFieldIndex(OWNER)].Trim();
            string strAssetLocation = arrFields[GetFieldIndex(LOCATION)].Trim();
            string strValueCIA = arrFields[GetFieldIndex(VALUE_CIA)].Trim();
            int valueCIA = Convert.ToInt32(strValueCIA);
            AssetCategory assetCategory = CreateAssetCategory(strAssetCategory);
            Role assetOwnerRole = CreateAssetOwnerRole(strAssetOwner);
            Location assetLocation = CreateAssetLocation(strAssetLocation);
            // See if asset already in cache, if not, create and add in cache!
            Asset asset = null;
            string key = string.Concat(strAssetCategory, strAssetName, strAssetOwner, strAssetLocation);
            if (!_assetCache.TryGetValue(key, out asset)) {
                // Create Asset object and add to cache.
                asset = new Asset() {
                    Category = assetCategory,
                    Location = assetLocation,
                    Name = strAssetName,
                    Owner = assetOwnerRole,
                    Project = this.Project,
                    ValueCIA = valueCIA
                };
                AssetRegisterService.SaveAsset(asset);
                _assetCache.Add(key, asset);
            }
            Debug.Assert(asset != null && asset.ID > 0);
            return asset;
        }

        private Location CreateAssetLocation(string strAssetLocation) {
            Location o = AssetRegisterService.GetAssetLocationByName(strAssetLocation);
            if (o == null) {
                o = new Location(strAssetLocation);
                AssetRegisterService.SaveAssetLocation(o);
                Debug.Assert(o.ID > 0);
            }
            return o;
        }

        private Role CreateAssetOwnerRole(string strAssetOwner) {
            Role o = AssetRegisterService.GetAssetOwnerRoleByName(strAssetOwner);
            if (o == null) {
                o = new Role(strAssetOwner);
                AssetRegisterService.SaveAssetOwnerRole(o);
                Debug.Assert(o.ID > 0);
            }
            return o;
        }

        private AssetCategory CreateAssetCategory(string strAssetCategory) {
            AssetCategory o = AssetRegisterService.GetAssetCategoryByName(strAssetCategory);
            if (o == null) {
                o = new AssetCategory(strAssetCategory);
                AssetRegisterService.SaveAssetCategory(o);
                Debug.Assert(o.ID > 0);
            }
            return o;
        }

        private void InitialiseHelperStructures() {
            // Clear the ASSET cache.
            _assetCache.Clear();
        }

        private void ValidateLine(int lineNumber, string[] arrFields) {
            if (arrFields.Length != GetNumberOfFields()) {
                throw new Exception(PrintErrorAtLine(lineNumber, string.Format("Expected {0} fields, found {1}", GetNumberOfFields(), arrFields.Length)));
            }
        }

        private string PrintErrorAtLine(int lineNumber, string errorMsg) {
            return string.Format("Line {0}: ERROR: {1}", lineNumber, errorMsg);
        }

    } // end internal class AssetRiskRegisterDataParser
}
